package my.mvc;

import com.fasterxml.jackson.databind.ObjectMapper;
import my.util.CryptUtils;
import my.util.RequestUtils;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import site.hackway.bean.User;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Created by hackway on 2016/5/15.
 */
public class RequestContext {
    private final static Logger log = LoggerFactory.getLogger(RequestContext.class);
    private final static int MAX_FILE_SIZE = (1 << 20) * 10;
    private final static int MAX_FILE_SIZE_100M = (1 << 20) * 100;
    private final static String UTF_8 = "UTF-8";

    private final static ThreadLocal<RequestContext> contexts = new ThreadLocal<>();

    public final static String COOKIE_LOGIN = "meganekkoid";
    private final static int MAX_AGE = 86400 * 365;
    private final static byte[] E_KEY = new byte[]{'.','M','E','G','A','N','E','K','K','O'};

    private ServletContext context;
    private HttpServletRequest request;
    private HttpServletResponse response;
    private Map<String, Cookie> cookies;
    private long beginTime = 0L;

    private static String webroot = null;
    private static String upload_tmp_path = null;
    private final static String TEMP_UPLOAD_PATH_ATTR_NAME = "$MEGANEKKO_TEMP_UPLOAD_PATH$";
    private ObjectMapper objectMapper = new ObjectMapper();

    // 自定义日期格式转换
    private final static Converter date_converter = new Converter() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat sdf_time = new SimpleDateFormat("yyyy-MM-dd H:mm:ss");

        @Override
        public Object convert(Class type, Object value) {
            if (value == null) {
                return null;
            }
            if (value.getClass().equals(type)) {
                return value;
            }
            Date date;
            try {
                date = sdf_time.parse(value.toString());
            } catch (ParseException e) {
                try {
                    date = sdf.parse(value.toString());
                } catch (ParseException e1) {
                    return null;
                }
            }
            if (type.equals(java.util.Date.class)) {
                return date;
            }
            if (type.equals(java.sql.Date.class)) {
                return new java.sql.Date(date.getTime());
            }
            if (type.equals(java.sql.Timestamp.class)) {
                return new java.sql.Timestamp(date.getTime());
            }
            return null;
        }
    };

    static {
        webroot = getWebrootPath();
        // 上传的临时目录
        upload_tmp_path = webroot + File.separator + "WEB-INF" + File.separator + "tmp" + File.separator;
        try {
            FileUtils.forceMkdir(new File(upload_tmp_path));
        } catch (IOException e) {
            log.error("create upload temp path error");
        }
        ConvertUtils.register(date_converter, java.util.Date.class);
        ConvertUtils.register(date_converter, java.sql.Date.class);
        ConvertUtils.register(date_converter, java.sql.Timestamp.class);
    }

    // 返回接收到该请求的时间
    public long getBeginTime() {
        return this.beginTime;
    }

    private final static String getWebrootPath() {
        String root = RequestContext.class.getResource("/").getFile();
        try {
            if (root.endsWith(".svn/")) {
                root = new File(root).getParentFile().getParentFile().getParentFile().getCanonicalPath();
            } else {
                root = new File(root).getParentFile().getParentFile().getCanonicalPath();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return root.endsWith(File.separator) ? root : root + File.separator;
    }

    public static RequestContext begin(ServletContext ctx, HttpServletRequest req, HttpServletResponse res) {
        RequestContext requestContext = new RequestContext();
        requestContext.context = ctx;
        requestContext.request = autoEncodingRequest(ctx, req);
        requestContext.response = res;
        requestContext.response.setCharacterEncoding(UTF_8);
        requestContext.cookies = new HashMap<>();
        requestContext.beginTime = System.currentTimeMillis();
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie: cookies) {
                requestContext.cookies.put(cookie.getName(), cookie);
            }
        }
        contexts.set(requestContext);
        requestContext.user();  //读取当前用户
        return requestContext;
    }

    public IUser getUserFromCookie() {
        try{
            Cookie cookie = cookie(COOKIE_LOGIN);
            if(cookie!=null && StringUtils.isNotBlank(cookie.getValue())){
                return userFromUUID(cookie.getValue());
            }
        }catch(Exception e){}
        return null;
    }

    /**
     * 从cookie中读取保存的用户信息
     * FIXME: 此方法可能导致严重的安全隐患
     * @param req
     * @return
     */
    public IUser userFromUUID(String uuid) {
        if(StringUtils.isBlank(uuid))
            return null;
        String ck = decrypt(uuid);
        final String[] items = StringUtils.split(ck, '|');
        if(items!=null && items.length == 5){
            //String ua = _CleanUserAgent(header("user-agent"));
            //int ua_code = ua.hashCode();
            //int old_ua_code = Integer.parseInt(items[3]);
            //if(ua_code == old_ua_code){
            return new IUser(){
                public boolean IsBlocked() { return false; }
                public long getId() { return NumberUtils.toLong(items[0],-1L); }
                public String getPwd() { return items[1]; }
                public int getRole() { return IUser.ROLE_GENERAL; }
                @Override
                public boolean IsRoleOf(int role) {
                    return false;
                }
            };
            //}
        }
        return null;
    }

    /**
     * 返回Web应用的路径
     * @return
     */
    public static String root() {
        return webroot;
    }

    public static String getContextPath() {
        RequestContext ctx = RequestContext.get();
        return (ctx!=null)?ctx.contextPath():"";
    }

    public void end() {
        String tmpPath = (String) request.getAttribute(TEMP_UPLOAD_PATH_ATTR_NAME);
        if (tmpPath != null) {
            try {
                FileUtils.deleteDirectory(new File(tmpPath));
            } catch (IOException e) {
                log.error("Failed to cleanup upload directory: " + tmpPath, e);
            }
        }
        this.context = null;
        this.request = null;
        this.response = null;
        this.cookies = null;
        contexts.remove();
    }

    public Locale locale() {
        return request.getLocale();
    }

    /**
     * 自动编码处理
     * @param req
     * @return
     */
    private static HttpServletRequest autoEncodingRequest(ServletContext context, HttpServletRequest req) {
        if (req instanceof RequestProxy) {
            return req;
        }
        String serverInfo = context.getServerInfo();
        if("POST".equalsIgnoreCase(req.getMethod())){
            try {
                req.setCharacterEncoding(UTF_8);
            } catch (UnsupportedEncodingException e) {}
        }

        return req;

    }

    /**
     *
     */
    private static class RequestProxy extends HttpServletRequestWrapper {

        private String uri_encoding;
        /**
         * Constructs a request object wrapping the given request.
         *
         * @param request
         * @param encoding
         * @throws IllegalArgumentException if the request is null
         */
        public RequestProxy(HttpServletRequest request, String encoding) {
            super(request);
            this.uri_encoding = encoding;
        }

        /**
         * The default behavior of this method is to return getParameterMap()
         * on the wrapped request object.
         */
        @Override
        public Map getParameterMap() {
            Map params = super.getParameterMap();
            HashMap<String, Object> new_params = new HashMap<>();
            Iterator it = params.keySet().iterator();
            while (it.hasNext()) {
                String key = (String) it.next();
                Object value = params.get(key);
                if (value.getClass().isArray()) {
                    String[] values = (String[]) params.get(key);
                    String[] new_values = new String[values.length];
                    for (int i=0;i<values.length;i++) {
                        new_values[i] = decodeParamValue(values[i]);
                    }
                    new_params.put(key, new_values);
                } else {
                    String oValue = (String) params.get(key);
                    String new_value = decodeParamValue(oValue);
                    if (new_value != null) {
                        new_params.put(key, new_value);
                    }
                }
            }
            return new_params;
        }

        /**
         * The default behavior of this method is to return
         * getParameter(String name) on the wrapped request object.
         *
         * @param name
         */
        @Override
        public String getParameter(String name) {
            String value = super.getParameter(name);
            return decodeParamValue(value);
        }


        /**
         * The default behavior of this method is to return
         * getParameterValues(String name) on the wrapped request object.
         *
         * @param name
         */
        @Override
        public String[] getParameterValues(String name) {
            String[] values = super.getParameterValues(name);
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    values[i] = decodeParamValue(values[i]);
                }
            }
            return values;
        }

        private String decodeParamValue(String value) {
            if (StringUtils.isBlank(value) || StringUtils.isBlank(uri_encoding) || StringUtils.isNumeric(value)) {
                return value;
            }
            try {
                return new String(value.getBytes("8859_1"), uri_encoding);
            } catch (Exception e) {

            }
            return value;
        }
    }

    /**
     * 获取当前请求的上下文
     * @return
     */
    public static RequestContext get() {
        return contexts.get();
    }

    /**
     * 生成用户登录标识字符串
     * @param user
     * @param ip
     * @param user_agent
     * @return
     */
    public static String genLoginKey(IUser user, String ip, String user_agent) {
        user_agent = cleanUserAgent(user_agent);
        StringBuilder sb = new StringBuilder();
        sb.append(user.getId());
        sb.append('|');
        sb.append(user.getPwd());
        sb.append('|');
        sb.append(ip);
        sb.append('|');
        sb.append(user_agent.hashCode());
        sb.append('|');
        sb.append(System.currentTimeMillis());
        return encrypt(sb.toString());
    }

    public static String cleanUserAgent(String ua) {
        if (StringUtils.isBlank(ua)) {
            return "";
        }
        int idx = StringUtils.indexOf(ua, "staticlogin");
        return idx>0?StringUtils.substring(ua, 0, idx):ua;
    }

    /**
     * 加密
     * @param value
     * @return
     * @throws Exception
     */
    public static String encrypt(String value) {
        return encrypt(value, E_KEY);
    }

    /**
     * 加密
     * @param value
     * @return
     * @throws Exception
     */
    public static String encrypt(String value, byte[] key) {
        byte[] data = CryptUtils.encrypt(value.getBytes(), key);
        try{
            return URLEncoder.encode(new String(Base64.encodeBase64(data)), UTF_8);
        }catch(Exception e){
            return null;
        }
    }

    /**
     * 解密
     * @param value
     * @return
     * @throws Exception
     */
    public static String decrypt(String value) {
        return decrypt(value, E_KEY);
    }

    /**
     * 解密
     * @param value
     * @return
     * @throws Exception
     */
    public static String decrypt(String value, byte[] key) {
        try {
            value = URLDecoder.decode(value,UTF_8);
            if(StringUtils.isBlank(value)) return null;
            byte[] data = Base64.decodeBase64(value.getBytes());
            return new String(CryptUtils.decrypt(data, key));
        } catch (Exception excp) {
            return null;
        }
    }

    public String ip() {
        String ip = RequestUtils.getRemoteAddr(request);
        if (ip == null) {
            ip = "127.0.0.1";
        }
        return ip;
    }

    public Enumeration<String> params() {
        return request.getParameterNames();
    }

    public String param(String name, String...def_value) {
        String v = request.getParameter(name);
        return (v!=null)?v:((def_value.length>0)?def_value[0]:null);
    }

    public long param(String name, long def_value) {
        return NumberUtils.toLong(param(name), def_value);
    }

    public int param(String name, int def_value) {
        return NumberUtils.toInt(param(name), def_value);
    }

    public byte param(String name, byte def_value) {
        return (byte)NumberUtils.toInt(param(name), def_value);
    }

    public String[] params(String name) {
        return request.getParameterValues(name);
    }

    public long[] lparams(String name){
        String[] values = params(name);
        if(values==null) return null;
        List<Long> lvs = new ArrayList<Long>();
        for(String v : values) {
            long lv = NumberUtils.toLong(v, Long.MIN_VALUE);
            if(lv != Long.MIN_VALUE && !lvs.contains(lv))
                lvs.add(lv);
        }
        long [] llvs = new long[lvs.size()];
        for(int i=0;i<lvs.size();i++)
            llvs[i] = lvs.get(i);
        return llvs;
    }

    public String uri(){
        return request.getRequestURI();
    }

    public String contextPath(){
        return request.getContextPath();
    }

    public void redirect(String uri) throws IOException {
        response.sendRedirect(uri);
    }

    public void forward(String uri) throws ServletException, IOException {
        log.info("requesst forward "+uri);
        RequestDispatcher requestDispatcher = context.getRequestDispatcher(uri);
        requestDispatcher.forward(request, response);
    }

    public void include(String uri) throws ServletException, IOException {
        RequestDispatcher rd = context.getRequestDispatcher(uri);
        rd.include(request, response);
    }

    public boolean isRobot(){
        return RequestUtils.isRobot(request);
    }


    public void error(int code, String...msg) throws IOException {
        if(msg.length>0)
            response.sendError(code, msg[0]);
        else
            response.sendError(code);
    }

    public void forbidden() throws IOException {
        error(HttpServletResponse.SC_FORBIDDEN);
    }

    public void not_found() throws IOException {
        error(HttpServletResponse.SC_NOT_FOUND);
    }

    public ServletContext context() { return context; }

    @Deprecated
    public HttpSession session() { return request.getSession(false); }
    @Deprecated
    public HttpSession session(boolean create) { return request.getSession(true); }
    @Deprecated
    public Object sessionAttr(String attr) {
        HttpSession ssn = session();
        return (ssn!=null)?ssn.getAttribute(attr):null;
    }
    public HttpServletRequest request() { return request; }
    public HttpServletResponse response() { return response; }
    public Cookie cookie(String name) { return cookies.get(name); }
    public void cookie(String name, String value, int max_age, boolean all_sub_domain) {
        RequestUtils.setCookie(request, response, name, value, max_age, all_sub_domain);
    }
    public Cookie deleteCookie(String name,boolean all_domain) { return RequestUtils.deleteCookie(request, response, name, all_domain); }
    public String header(String name) { return request.getHeader(name); }
    public void header(String name, String value) { response.setHeader(name, value); }
    public void header(String name, int value) { response.setIntHeader(name, value); }
    public void header(String name, long value) { response.setDateHeader(name, value); }
    public String user_agent() { return header("user-agent"); }
    public int user_agent_code() {
        String ua = user_agent();
        return (ua!=null)?Math.abs(ua.hashCode()):0;
    }
    /**
     * 设置public缓存，设置了此类型缓存要求此页面对任何人访问都是同样数据
     * @param minutes 分钟
     * @return
     */
    public void setPublicCache(int minutes) {
        if(!"POST".equalsIgnoreCase(request.getMethod())){
            int seconds = minutes * 60;
            header("Cache-Control","max-age="+seconds);
            Calendar cal = Calendar.getInstance(request.getLocale());
            cal.add(Calendar.MINUTE, minutes);
            header("Expires", cal.getTimeInMillis());
        }
    }

    /**
     * 设置私有缓存
     * @param minutes
     * @return
     */
    public void setPrivateCache(int minutes) {
        if(!"POST".equalsIgnoreCase(request.getMethod())){
            header("Cache-Control","private");
            Calendar cal = Calendar.getInstance(request.getLocale());
            cal.add(Calendar.MINUTE, minutes);
            header("Expires", cal.getTimeInMillis());
        }
    }

    /**
     * 关闭缓存
     */
    public void closeCache(){
        header("Pragma","must-revalidate, no-cache, private");
        header("Cache-Control","no-cache");
        header("Expires", "Sun, 1 Jan 2000 01:00:00 GMT");
    }

    /**
     * 将HTTP请求参数映射到bean对象中
     * @param req
     * @param beanClass
     * @return
     * @throws Exception
     */
    public <T> T form(Class<T> beanClass) {
        try{
            T bean = beanClass.newInstance();
            BeanUtils.populate(bean, request.getParameterMap());
            return bean;
        }catch(Exception e) {
            throw new ActionException(e.getMessage());
        }
    }

    /**
     * 返回当前登录的用户资料
     * @return
     */
    public IUser user() {
        return User.GetLoginUser(request);
    }

    public void saveUserInCookie(IUser user, boolean save) {
        String new_value = genLoginKey(user, ip(), header("user-agent"));
        int max_age = save?MAX_AGE:-1;

    }
    public void deleteUserInCookie() {
        deleteCookie(COOKIE_LOGIN, true);
    }

    public String body(String charset) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        IOUtils.copy(request.getInputStream(), out);
        return out.toString(charset);
    }

    public void print(Object msg) throws IOException {
        if (!UTF_8.equalsIgnoreCase(response.getCharacterEncoding())) {
            response.setCharacterEncoding(UTF_8);
        }
        response.getWriter().print(msg);
    }
    public void printJSON(Object msg) throws IOException {
        response.setContentType("application/json");
        print(msg);
    }
    public void printMapToJson(Map value) throws IOException{
        print(objectMapper.writeValueAsString(value));
    }

    /**
     * job格式化返回json数据，失败
     * @param msg 信息
     * @param data 额外数据
     * @throws IOException
     */
    public void json_result_failed(String msg, Object data) throws IOException {
        Map<String,Object> map = new HashMap<String,Object>();

        map.put("result",false);
        map.put("msg",msg);
        map.put("data",data);

        print(objectMapper.writeValueAsString(map));

        this.header("Content-type", "application/json");
    }

    /**
     * job格式化返回json数据，成功
     * @param msg 信息
     * @param data 额外数据
     * @throws IOException
     */
    public void json_result_success(String msg, Object data) throws IOException {
        Map<String,Object> map = new HashMap<String,Object>();

        map.put("result",true);
        map.put("msg",msg);
        map.put("data",data);

        print(objectMapper.writeValueAsString(map));

        this.header("Content-type", "application/json");
    }
}
